//
//  PLPlatform.m
//  Plankton
//
//  Created by Marc Schlichte on 2008-07-12.
//  Copyright 2008 Framework Labs. All rights reserved.
//

#import "PLPlatform.h"

#import "PLEndpoint.h"


#pragma mark handlers

@implementation PLTargetActionPair

- (id)initWithTarget:(id)target_ action:(SEL)action_
{
    if (self = [super init]) {
        target = target_; // weak-ref
        action = action_;
    }
    return self;
}

@end


@implementation PLTargetActionMessageHandler

+ (PLTargetActionMessageHandler *)handlerWithTarget:(id)target action:(SEL)action
{
    return [[[PLTargetActionMessageHandler alloc] initWithTarget:target action:action] autorelease];
}

- (void)handleMessage:(id)message forPattern:(PLPattern *)pattern
{
    [target performSelector:action withObject:message withObject:pattern];
}

@end


@implementation PLTargetActionTimeoutHandler

+ (PLTargetActionTimeoutHandler *)handlerWithTarget:(id)target action:(SEL)action
{
    return [[[PLTargetActionTimeoutHandler alloc] initWithTarget:target action:action] autorelease];
}

- (void)handleTimeoutForPattern:(PLPattern *)pattern;
{
    [target performSelector:action withObject:pattern];
}

@end


#pragma mark patterns

@interface PLPredicatePattern : PLPattern
{
    NSPredicate *predicate;
}

- (PLPredicatePattern *)initWithPredicate:(NSPredicate *)pred;

@end


@interface PLOneOfPattern : PLPattern
{
    NSArray *patterns;
}

- (id)initWithPatterns:(NSArray *)patterns;

@end


@implementation PLPattern

@synthesize userInfo;

+ (PLPattern *)patternAny
{
    return [[[PLPattern alloc] init] autorelease];
}

+ (PLPattern *)patternWithPredicate:(NSPredicate *)predicate
{
    return [[[PLPredicatePattern alloc] initWithPredicate:predicate] autorelease];
}

+ (PLPattern *)patternWithPredicateFormat:(NSString *)predicateFormat, ...
{
    va_list ap;
    va_start(ap, predicateFormat);
    PLPattern *res = [self patternWithPredicate:[NSPredicate predicateWithFormat:predicateFormat arguments:ap]];
    va_end(ap);
    return res;
}

- (void)dealloc
{
    [userInfo release];
    [timeoutTimer release];
    [timeoutHandler release];
    [messageHandler release];
    [super dealloc];
}

- (PLPattern *)setMessageHandler:(id<PLMessageHandler>)handler
{
    [messageHandler autorelease];
    messageHandler = [handler retain];
    return self;
}

- (id<PLMessageHandler>)messageHandler
{
    return messageHandler;
}

- (PLPattern *)setMessageTarget:(id)target action:(SEL)action
{
    return [self setMessageHandler:[PLTargetActionMessageHandler handlerWithTarget:target action:action]];
}

- (PLPattern *)setTimeoutHandler:(id<PLTimeoutHandler>)timeout after:(NSTimeInterval)interval
{
    [timeoutHandler autorelease];
    timeoutHandler = [timeout retain];
    timeoutInterval = interval;
    return self;
}

- (id<PLTimeoutHandler>)timeoutHandler
{
    return timeoutHandler;
}

- (PLPattern *)setTimeoutTarget:(id)target action:(SEL)action after:(NSTimeInterval)interval
{
    return [self setTimeoutHandler:[PLTargetActionTimeoutHandler handlerWithTarget:target action:action] after:interval];
}

- (NSTimeInterval)timeoutInterval
{
    return timeoutInterval;
}

- (void)setTimeoutTimer:(NSTimer *)timer
{
    [timeoutTimer autorelease];
    timeoutTimer = [timer retain];
}

- (void)invalidate
{
    [timeoutTimer invalidate];
}

- (PLPattern *)matches:(id)message
{
    return self; // this is for the any case, overwrite in subclasses!
}

@end


@implementation PLPredicatePattern

- (PLPredicatePattern *)initWithPredicate:(NSPredicate *)pred
{
    if (self = [super init]) {
        predicate = [pred copy];
    }
    return self;
}

- (void)dealloc
{
    [predicate release];
    [super dealloc];
}

- (PLPattern *)matches:(id)message
{
    @try {
        return [predicate evaluateWithObject:message] ? self : nil;
    }
    @catch (NSException *e) {
    }
    return nil;
}

@end


@implementation PLOneOfPattern

- (id)initWithPatterns:(NSArray *)patterns_
{
    if (self = [super init]) {
        patterns = [patterns_ copy];
    }
    return self;
}

- (void)dealloc
{
    [patterns release];
    [super dealloc];
}

- (PLPattern *)matches:(id)message
{
    for (PLPattern *pattern in patterns) {
        if ([pattern matches:message]) {
            return pattern;
        }
    }
    return nil;
}

@end


#pragma mark platform

@interface PLPlatform (InternalMethods)

- (void)postData:(NSData *)data;

- (BOOL)deliverAnyMessageForPattern:(PLPattern *)pattern;
- (BOOL)deliverMessage:(id)message forPattern:(PLPattern *)pattern;

- (id)internalizeFromData:(NSData *)data;
- (NSData *)externalizeToData:(id)message;

- (NSString *)aidForHost:(NSString *)host port:(unsigned short)port name:(NSString *)name;

- (void)noteProblem:(NSString *)problem inSubsystem:(NSString *)subsystem;

@end


@implementation PLPlatform

- (id)init
{
    if (self = [super init]) {
        patterns = [[NSMutableArray alloc] init];
        messages = [[NSMutableArray alloc] init];
        endpoint = [[PLEndpoint alloc] init];
        endpoint.delegate = self;
    }
    return self;
}

- (void)dealloc
{
    [endpoint release];
    [patterns release];
    [messages release];
    [super dealloc];
}

- (NSString *)_createAidWithName:(NSString *)name
{
    return [self aidForHost:[endpoint hostname] port:[endpoint port] name:name];
}

- (NSString *)createAid
{
    return [self _createAidWithName:[NSString stringWithFormat:@"%d", nextLocalAid++]];
}

+ (int)portOfAid:(NSString *)aid
{
    NSURL *url = [NSURL URLWithString:aid];
    return [[url port] intValue];
}


- (void)send:(id)message to:(NSString *)aid
{   
    // convert message to data object
    NSData *data = [self externalizeToData:message];
    if (!data) {
        [NSException raise:NSInvalidArgumentException format:@"message to send is nil or is not in plist format"];
    }
    
    // convert aid to host and port
    NSURL *url = [NSURL URLWithString:aid];
    NSString *host = [url host];
    unsigned short port = [[url port] unsignedShortValue];
    if (!host || !port) {
        [NSException raise:NSInvalidArgumentException format:@"invalid host or port of target aid:%@", aid];
    }
    
    // if we send to same platform, post it directly, otherwise send through endpoint 
    if (port == endpoint.port && [host isEqual:endpoint.hostname]) {
        [self postData:data];
    }
    else {
        [endpoint sendData:data toHost:host port:port];
    }
}

- (void)sendArrayTo:(NSString *)aid withObjects:(id)first, ...
{
    NSMutableArray *array = [NSMutableArray array];
    if (first) {
        [array addObject:first];
        id obj;
        va_list ap;
        va_start(ap, first);
        while (obj = va_arg(ap, id)) {
            [array addObject:obj];
        }
        va_end(ap);
    }
    [self send:array to:aid];
}

- (PLPattern *)receive:(PLPattern *)pattern
{
    // store away the new pattern
    [patterns addObject:pattern];
    
    // check if this new pattern matches any of the pending messages
    BOOL deliveredAny = [self deliverAnyMessageForPattern:pattern];

    // start timout timers if indicated
    if (!deliveredAny && [pattern timeoutHandler]) {
        NSTimeInterval interval = [pattern timeoutInterval];
        NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                          target:self 
                                                        selector:@selector(onTimer:) 
                                                        userInfo:pattern 
                                                        repeats:NO];
        [pattern setTimeoutTimer:timer];
        
        // fire timer immediatly if interval is zero
        if (interval == 0.0) {
            [timer fire];
        }
    }
    
    // return pattern for call chain convenience
    return pattern;
}

- (void)onTimer:(NSTimer *)timer
{
    PLPattern *pattern = [timer userInfo];
    
    // remove pattern from list of patterns
    [pattern retain];
    [patterns removeObject:pattern];
    
    // call the timeout
    [[pattern timeoutHandler] handleTimeoutForPattern:pattern];
    
    // give up upon pattern
    [pattern release];
}

- (PLPattern *)receiveOneOf:(NSArray *)patterns_
{
    return [self receive:[[[PLOneOfPattern alloc] initWithPatterns:patterns_] autorelease]];
}

- (NSNumber *)createTag
{
    return [NSNumber numberWithUnsignedLongLong:++nextTag];
}

- (void)endpoint:(PLEndpoint *)endpoint didReceiveData:(NSData *)data from:(NSData *)addr
{
    [self postData:data];
}

- (void)endpoint:(PLEndpoint *)endpoint encounteredProblem:(NSString *)problem
{
    [self noteProblem:problem inSubsystem:@"endpoint"];
}

- (void)postData:(NSData *)data
{
    id message = [self internalizeFromData:data];
    
    for (PLPattern *pattern in [patterns copy]) {
        if ([self deliverMessage:message forPattern:pattern]) {
            return;
        }
    }       
    
    // could not immediatly deliver message, so add to message queue
    [messages addObject:message];
}

- (BOOL)deliverAnyMessageForPattern:(PLPattern *)pattern
{
    for (id message in [messages copy]) {
        if ([self deliverMessage:message forPattern:pattern]) {
            [messages removeObject:message];
            return YES;
        }
    }
    return NO;
}

- (BOOL)deliverMessage:(id)message forPattern:(PLPattern *)pattern
{
    PLPattern *matchedPattern = [pattern matches:message];
    if (matchedPattern) {
        
        // remove pattern from list of patterns but don't free it yet.
        [pattern retain];
        [patterns removeObject:pattern];
        
        // stop any timers
        [pattern invalidate];
        
        // let handler of matched-pattern handle the message
        id<PLMessageHandler> handler = [matchedPattern messageHandler];
        if (handler) {
            [handler handleMessage:message forPattern:matchedPattern];
        }
        
        // now free the pattern
        [pattern release];
        
        return YES;
    }
    else {
        return NO;
    }
}

- (id)internalizeFromData:(NSData *)data
{
    NSString *error;
    id res = [NSPropertyListSerialization propertyListFromData:data 
                                              mutabilityOption:NSPropertyListImmutable 
                                                        format:NULL 
                                              errorDescription:&error];
    if (error) {
        [self noteProblem:error inSubsystem:@"internalizeData"];
        [error release];
    }
    return res;
}

- (NSData *)externalizeToData:(id)msg
{
    NSString *error;
    NSData *res = [NSPropertyListSerialization dataFromPropertyList:msg 
                                                             format:NSPropertyListBinaryFormat_v1_0
                                                   errorDescription:&error];
    if (error) {
        [self noteProblem:error inSubsystem:@"externalizeData"];
        [error release];
    }
    return res;
}

- (NSString *)aidForHost:(NSString *)host port:(unsigned short)port name:(NSString *)name
{
    // actor-ids (aids) will be of form: plap://machine:port/name
    return [NSString stringWithFormat:@"plap://%@:%d/%@", host, port, name];
}

- (void)noteProblem:(NSString *)problem inSubsystem:(NSString *)subsystem
{
    NSLog(@"problem in '%@': %@", subsystem, problem);
}

@end
